Scrapy是一个纯Python语言实现的爬虫框架,简单、易用、拓展性高使得其成为Python爬虫中的主流利器,本文以目前官方最新的版本1.6为基础,展开从简单使用到深入原理的探讨。
提前说一下教程归教程,总归还是没有官方文档讲的贴切!如果读者阅读完本文对Scrapy产生了兴趣并原意更深入了解Scrapy的话,请一定养成随时翻阅官方文档的习惯!
本文主要阐述以下内容
- 为什么选择Scrapy?
- Hello Scrapy!(实践)
- Scrapy如何工作的?
对于第一小节『为什么选择Scrapy』建议读者都能阅览一下,我会分析一下我对Scrapy的业务场景的理解。
对于余下的两个小节,我原意是将『Scrapy是如何工作的』放在『Hello Scrapy』之前去讲的,但是考虑到并非所有人都愿意一上来就了解理论性的东西,所以就先把实践性的小Demo放在前面讲,希望以此引起读者的兴趣,兴趣能让我们更深入的了解一件事。因此我就把『Scrapy如何工作』这一节放在最后讲,也可以承接下一章的Scrapy原理!
虽然Scrapy已经被设计的能够满足绝大多数的爬虫工作,但还是有一些场景其实并不适用。
- 什么情况Scrapy不是首选?
当你的爬取页面数量很少,针对的站点规模很小的时候,Scrapy并不是首选。例如爬取点电影榜单,某些新闻资讯等等,Requests+PyQuery这种方式就已经能够很好的完成此类任务,产生的代码量比Scrapy少,并且从网络请求效率以及网页解析速度上Requests和PyQuery都比Scrapy自带的两个模块要好!
没有通用性爬虫需求时,Scrapy可选可不选。在我看来Scrapy真正的好处在于能够针对多种不同类型的网站定制相应的『Spider动作』,强大的『ItemLoader』能够对数据输入输出定义一系列的处理动作。假如你没有需要不断的拓展信息源的需求,Scrapy其实并不能发挥最大的能力!
当你需要增量爬取数据时,Scrapy显得很无力。Scrapy并没有增量爬取的功能实现,因为增量的难度不一样,如果简单需求对Scrapy进行小手术估计就能完成了,但是如果是增量要求高的话,可能Scrapy真的动起来很麻烦!
注意:以上三种情况只是想说明Scrapy不是首选,并没有说不建议用!只是希望读者能够明白在选择一个框架或技术的时候不是跟风,在设计之初就考虑慎重对项目的良好发展有很大的益处。
- 什么情况Scrapy很好用?
需要分布式设计时,Scrapy的非官方组件Scrapy-redis很好用。Scrapy本身也并不能实现分布式机制,但是使用rmax所开发的Scrapy-redis就可以实现分布式,后面我也会慢慢讲到。
有可拓展需求时,Scrapy是利器。具体原因在上面已经阐述过,这里就不多做解释了。
注意:以上所有情况均来自我个人使用Scrapy时的总结,仅供参考!
Demo以豆瓣(万古爬虫受害者)热门电影排行榜以及其所有评论为实验目标,一一讲述Scrapy的基本功能,相信读者在实践完这个Demo之后,就能很好的使用Scrapy了。
需要安装:
- python (本文所使用的是3.7)
- scrapy
安装环境
- 安装Scrapy
命令行中键入pip install scrapy
创建Scrapy项目
在命令行中键入scrapy startproject douban_demo
,结果如下图所示
之后可以看到Scrapy还提示我们可以使用genspider
这个命令来创建我们的爬虫文件,在这之前我们先来看看刚刚那条命令执行完之后发生了什么。
查看文件目录。我们可以看到如下信息
douban_demo
├── douban_demo
│ ├── items.py
│ ├── middlewares.py
│ ├── pipelines.py
│ ├── settings.py
│ └── spiders
└── scrapy.cfg
2
3
4
5
6
7
8
大致了解每个文件用途之后,接下来我们就开始我们的爬虫之旅吧。
描述一个爬虫
使用scrapy genspider douban douban.com
来新建一个爬虫文件,这个新建的爬虫文件会被放入douban_demo/spiders
底下。
PS:genspider
的用法scrapy genspider [options] <name> <domain>
此时douban.py
文件就会出现在spiders
底下,初始内容如下:
import scrapy
class DoubanSpider(scrapy.Spider):
name = 'douban'
allowed_domains = ['douban.com']
start_urls = ['http://douban.com/']
def parse(self, response):
pass
2
3
4
5
6
7
8
9
10
11
在Scrapy项目中你所有的Spider类都必须得继承scrapy.Spider
,其中name
、start_urls
以及parse
成员方法是每个Spider类必须要声明的。更多的Spider属性以及成员方法可以点击此链接
接下来只要将我们的爬取对象链接放入start_urls
里面就可以,我们以https://movie.douban.com/chart
为实验对象。
将DoubanSpider
中的start_urls
的值替换为 start_urls = ['https://movie.douban.com/chart']
使用shell方式进行页面测试
Scrapy还给我们提供了shell
命令供我们在shell
中进行页面数据提取测试,这比requests+pyquery的方式要高效。
命令格式:scrapy shell urls
在命令行里键入scrapy shell
进入shell
模式。
注意:此时不要着急添加urls,因为我们的测试对象有对UA进行检测,如果直接入测试链接会出现403。至于在什么目录输入这条命令不做具体限制。
输出内容如下:
(venv) ➜ douban_demo scrapy shell --nolog
[s] Available Scrapy objects:
[s] scrapy scrapy module (contains scrapy.Request, scrapy.Selector, etc)
[s] crawler <scrapy.crawler.Crawler object at 0x106c5c550>
[s] item {}
[s] settings <scrapy.settings.Settings object at 0x108e18898>
[s] Useful shortcuts:
[s] fetch(url[, redirect=True]) Fetch URL and update local objects (by default, redirects are followed)
[s] fetch(req) Fetch a scrapy.Request and update local objects
[s] shelp() Shell help (print this help)
[s] view(response) View response in a browser
2
3
4
5
6
7
8
9
10
11
此时我们就可以看到已经进入了类似Python命令行交互器一样的界面,首先我们为了防止豆瓣403应该在settings
里面加入DEFAULT_REQUEST_HEADERS
属性,这是一个请求头字典,只要Scrapy检测到有这个选项都会将里面的值加入到请求头中。
值如下:
DEFAULT_REQUEST_HEADERS = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 \
(KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36'
}
2
3
4
5
6
在交互界面中键入一下内容即可添加默认请求头
>>> settings.DEFAULT_REQUEST_HEADERS = {
... 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
... 'Accept-Language': 'en',
... 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 \
... (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36'
... }
2
3
4
5
6
再次输入settings.DEFAULT_REQUEST_HEADERS
查看是否添加成功。
配置完成后我们就可以使用fetch(urls)
命令来抓取我们需要测试的页面了
键入fetch('https://movie.douban.com/chart')
即可看到一下内容
2019-06-03 23:06:13 [scrapy.core.engine] INFO: Spider opened
2019-06-03 23:06:13 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://movie.douban.com/robots.txt> (referer: None)
2019-06-03 23:06:14 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://movie.douban.com/chart> (referer: None)
2
3
从日志中我们可以看到已经成功获取了目标页面,在获取页面之前我们还可以知道scrapy先访问了robots.txt
文件,这是一个良好的爬虫习惯,此时scrapy的所有页面获取都将会遵从robots.txt
里面的规则,如果你不想遵从这一规则可以在settings
里配置ROBOTSTXT_OBEY = False
。
此时你可以使用response.text
来检查我们是否获取了整个页面的源码。 scrapy的所有资源解析操作都被集成在了response
这个对象中,更多的response
介绍可以点击此链接
分析页面
电影排行榜页面
对页面进行元素检查
可以看到我们需要爬取的内容在table
里面。因为页面有多个table
,因此只需要对其迭代获取即可。
在shell
中使用response.css('table')
即可获取所有的table
元素,本文全部采用css selector
进行元素选择,xpath
也可自行切换。
每个电影的信息都在table
标签底下的tr.item
里面。
电影的详情链接可以使用a.nbg::attr(href)
来获取
电影图片我们可以使用a.nbg > img::attr(src)
来获取
对于电影名字处理稍显复杂,从上图可以看出电影可能拥有多个名字,都被包裹在div.pl2 > a
底下,其中其他名字在div.pl2 > a > span
底下,因此我们需要对名字进行一些格式处理,例如去掉空格、换行符等等。
因此影名可以使用div.pl2 > a::text
和div.pl2 > a > span::text
分别获取,但是因此div.pl2
底下的a
标签较多,我们只需要取得第一个即可使用extract_first()
方法即可取出第一个Selector
元素的内容并转换为str
。
电影简介只需要使用p.pl::text
中获取即可
电影评论页面
在相应的电影详细信息链接后拼接comments?status=P
即可进入电影影评页面。
可以看出影评数据由多个comment-item
组成,影评内容都被包裹在div.comment
底下,因此按照上面的分析方法也能找出相应数据的获取方式。这里就不在阐述
实现思路
分别创建两个
parse
方法:parse_rank
和parse_comments
,parse_rank
负责处理电影排行榜页面,parse_comments
负责处理相应的评论页面。重写
Spider
类的start_requests
方法,填充url
以及callback
属性值,由于要先通过电影排行榜页面获取详情信息才可获取想关评论地址,所以在start_requests
中返回的Request callback
属性应该填充为self.parse_rank
在
parse_rank
中处理返回的reponse
,按照『分析页面』中的思路去解析数据并且使用yield
抛出评论页面的Request
,callback
属性填充为self.parse_comments
在
parse_comments
方法中处理返回的评论页面,抛出数据以及下一页链接。
注:Spider parse
方法:所有的parse
方法都必须返回Item(目前可以理解为数据项)或者Requests(下一条请求)。这里所有的parse
的意思是不是特指Spider
类中生成的parse
方法,而是所有具备解析功能的函数都应该返回Item或者Requests。
代码示例
import scrapy
from scrapy.http.request import Request
class DoubanSpider(scrapy.Spider):
name = 'douban'
def start_requests(self):
yield Request(url='https://movie.douban.com/chart', callback=self.parse_rank)
def parse_rank(self, response):
for item in response.css('tr.item'):
detail_url = item.css('a.nbg::attr(href)').extract_first()
img_url = item.css('a.nbg > img::attr(src)').extract_first()
main_name = item.css('div.pl2 > a::text').extract_first()
other_name = item.css('div.pl2 > a > span::text').extract_first()
brief = item.css('p.pl::text').extract_first()
main_name = main_name.replace('\n', '').replace(' ', '')
yield {
'detail_url': detail_url,
'img_url': img_url,
'name': main_name+other_name,
'brief': brief
}
yield Request(url=detail_url+'comments?status=P',
callback=self.parse_comments,
meta={'movie': main_name})
def parse_comments(self, response):
for comments in response.css('.comment-item'):
username = comments.css('span.comment-info > a::text').extract_first()
comment = comments.css('span.short::text').extract_first()
yield {
'movie': response.meta['movie'],
'username': username,
'comment': comment
}
nexturl = response.css('a.next::attr(href)').extract_first()
if nexturl:
yield Request(url=response.url[:response.url.find('?')]+nexturl,
callback=self.parse_comments,
meta=response.meta)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
启动爬虫
一切准备就绪,我们就可以在douban_demo
(最顶级的)目录底下键入命令 scrapy crawl douban
就可以看到有许多的日志数据并且还打印出了许多电影信息以及评论内容。
到此我们就对豆瓣电影排行以及评论完成了初步的抓取,当然豆瓣限制了非登陆用户可以查看的评论数以及检测爬虫行为等等,这些反爬机制我们日后再讲。
那么现在有一个问题是我需要将数据保存应该如何做呢?
Scrapy提供了许多Feed exports
的方法,可以将输出数据保存为json, json lines, csv, xml
在启动命令后面加-o xx.json
就可以将文件保存为json
格式。
如:scrapy crawl douban -o result.json
因为数据有中文内容,scrapy在使用json encoder
的时候默认所有数据均是ascii
的,因此我们需要将数据编码设置为utf-8
。
只需在settings.py
中加入FEED_EXPORT_ENCODING = 'utf-8'
即可。
这时候在此数据即可看到中文正常显示。
此时大约会产生2000条数据。
小结
到此我们已经完成了对豆瓣电影及影评的初步抓取,虽然能够成功抓取数据,但给人的感觉就是『我仅仅只是编写了解析网页的代码以及键入启动爬虫命令,结果Scrapy就能够帮我完成从网页请求到数据产出所有的任务』,我们得继续探究当我们键入了scrapy crawl douban -o result.json
这一条启动命令后Scrapy到底做了什么。
请各位原意了解Scrapy的读者保存下图,此图对Scrapy的学习尤为关键。
根据此图分析,当我们键入scrapy crawl douban -o result.json
之后,Scrapy做了以下工作
Crawler
接收到crawl
指令后便会被启动,激活name
为douban
的Spider
,同时创建Engine
,此时我们的DoubanSpider
就被启动。当
DoubanSpider
被新建之后,Engine
就会检测Spider
的请求队列,也就是我们的start_urls
属性orstart_requests
方法。这两者都必须是可迭代对象,因此可以理解我们的示例代码中start_requests
方法为何是使用yield
抛出。此时生成Request
对象,所有Request
对象都会先经过Spider Middlewares
这个中间件,现阶段我们只需要将中间件理解为一座座桥,我们现在不必深究桥上有什么。Spider
产生的Request
对象会经过Engine
送入Scheduler
调度器中,调度器会将所有Request
加入请求队列,一旦可以调度之后,Request
就会通过Downloader Middlewares
这些桥梁到达Downloader
,Downloader
就会根据请求内容访问指定的互联网资源,这一过程是异步的。当
Downloader
完成一个Request
任务后,就会将资源包装成一个Response
,里面会包含原Request
的信息、封装好的解析器等等,在示例中我们可以看到在parse_rank
中抛出的Request
携带着meta
数据,之后meta
继续保存在parse_comments
的response
里。此时所有的
Response
都会再次通过Downloader Middlewares
这些桥,再经过Engine
以及Spider Middlewares
回到所对应的Spider
中,并且会激活对应的callback
函数,最后就是执行我们编写好的parse
方法里的代码。当parse
再次抛出Request
对象时就会重新执行(3-5)的步骤。当
Spider
抛出数据(Item)时,又会再次经过Spider Middlewares
到达Item Pipeline
,但我们并没有对Item Pipeline
指定任何动作因此它只会向外界抛出该Item
,之后会被logger
捕获这一输出,也就是我们可以看到的控制台有数据产生,由于我们使用了-o
指令,因此exporter
会将item
输出为相应的格式,也就有了我们指定的result.json
数据集。
到此我们完成了如何使用Scrapy编写一个简单爬虫程序,以及大致了解了Scrapy的工作流程,接下来我们会更加深入的探讨Scrapy的其他组件以及如何利用它们突破反爬机制。
如上述观点有误欢迎雅正!